Criando aplicações em tempo real com SSE (Server-Sent Events)
Há cerca de uma década, se precisássemos desenvolver uma aplicação em tempo real, como um chat ou um feed de atualizações, a primeira solução que viria à mente seria o uso de polling. Essa técnica consiste em enviar requisições HTTP de forma temporizada e recorrente ao servidor, buscando dados atualizados. No entanto, mesmo quando não há novas informações disponíveis, essas requisições continuam sendo disparadas, resultando em desperdício de recursos, como largura de banda e processamento do servidor.

Felizmente, os tempos mudaram. Hoje, ao trabalharmos com JavaScript, contamos com a biblioteca EventSource, que permite estabelecer uma conexão SSE (Server-Sent Events). Neste artigo, explorarei os conceitos por trás desse recurso e apresentarei um breve tutorial para aplicar esses conceitos na prática.
Uma conexão sempre aberta
Diferentemente das requisições HTTP convencionais, onde o cliente dispara uma requisição e o servidor devolve uma resposta, as conexões SSE permanecem sempre abertas. Isso possibilita uma comunicação unidirecional entre servidor e cliente. Ao contrário dos WebSockets, que permitem comunicação bidirecional, no SSE apenas o servidor envia dados ao cliente, que os recebe de forma instantânea enquanto está conectado.

Uma vez estabelecida a conexão, os dados chegam ao cliente JavaScript na forma de eventos, eliminando a necessidade de disparar novas requisições ao servidor para buscar atualizações, como acontece no polling. Cabe ao servidor a responsabilidade de enviar eventos com os dados atualizados sempre que necessário.

A menos que a conexão seja encerrada, o cliente fica constantemente aguardando novos eventos de dados do servidor, tornando essa técnica ideal para a construção de notificações, dashboards ou chats que necessitam de atualizações constantes.
Sistema de controle de conexões
Para demonstrar na prática os conceitos abordados, vamos criar uma aplicação em Node.js (back-end) responsável por disponibilizar um endpoint de conexão SSE, que será utilizado por um cliente JavaScript (front-end).
Nossa aplicação consiste em um sistema onde o usuário deve informar um UserID e estabelecer uma conexão SSE. A partir disso, o servidor começa a disparar, a cada 5 segundos, um evento que retorna a lista de usuários conectados. Caso o usuário encerre sua conexão, ele é removido automaticamente da lista de usuários conectados.
Vamos começar!
npm init -y
npm i express
Ao criar o arquivo index.js
, vamos centralizar a lógica do nosso back-end. Além disso vamos criar um pasta /public
onde ficarão o index.html
e arquivo script.js
para gerenciar nossa página. A estrutura deve ficar assim:
/public
index.html
script.js
index.js
package.json
Em index.js
, vamos importar a biblioteca express
, que é responsável por permitir a criação de endpoints HTTP:
import express from "express"
const app = express()
app.use(express.json())
const PORT = 3000
app.listen(PORT, () => console.log(`Server is running on ${PORT} port`))
Além disso, é necessário configurar para que o conteúdo da pasta /public
seja retornado quando o usuário acessar http://localhost:3000/
:
import express from "express"
import fs from "fs"
import path from "path"
const app = express()
app.use(express.json())
app.use(express.static(path.join(path.resolve(path.dirname("")), "public")))
app.get("/", (_, res) => {
res.writeHead(200, { "content-language": "text/html" })
const streamIndexHtml = fs.createReadStream("index.html")
streamIndexHtml.pipe(res)
})
const PORT = 3000
app.listen(PORT, () => console.log(`Server is running on ${PORT} port`))
Dentro de /public
em index.html
vamos montar um HTML básico para retornar um título e testar o funcionando do servidor:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Server Sent Events Demo</title>
</head>
<body>
<h1>Server Sent Events Demo (SSE)</h1>
</body>
</html>
Agora, ao rodar o comando npm start
no terminal, a página já deve ser exibida no navegador:

Voltando as atenções para o back-end, podemos criar arquivo connection.js
, responsável por gerenciar as conexões do usuários ao servidor:
/public
index.html
script.js
index.js
connection.js
package.json
Dentro dele, vamos exportar uma classe Connection
, onde será armazenado um mapa de usuários conectados, além dos métodos registerUser
, para registrar novos usuários, e removeUser
, para removê-los do mapa:
export default class Connection {
_users = new Map()
registerUser() {}
removeUser() {}
}
Também podemos criar um getter connectedUsers
para facilitar o acesso aos usuários conectados fora da classe:
export default class Connection {
_users = new Map()
get connectedUsers() {
return [...this._users.keys()]
}
registerUser() {}
removeUser() {}
}
No método registerUser, vamos receber o userId
do usuário conectado e gerar um connectionId
, que servirá como identificador único para essa conexão estabelecida:
registerUser(userId, response) {
if (!this._users.has(userId)) {
this._users.set(userId, [])
}
const connectionId = this.generateConnectionId()
this._users.get(userId).push({ connectionId, response })
return connectionId
}
No método generateConnectionId
, podemos utilizar a biblioteca crypto
para gerar e retornar um token aleatório:
generateConnectionId() {
return crypto.randomBytes(20).toString("hex")
}
Já o método removeUser
receberá o userId
e o connectionId
do usuário que fechou a conexão, e, com isso, removerá o usuário do mapa:
removeUser(userId, connectionId) {
if (!this._users.has(userId)) {
return
}
const connections = this._users
.get(userId)
.filter((connection) => connection.connectionId !== connectionId)
if (connections.length) {
this._users.set(userId, connections)
} else {
this._users.delete(userId)
}
}
Como a classe Connection
isola a lógica de gerenciamento das conexões, precisamos configurar um endpoint em nosso servidor (index.js
) para que os clientes possam iniciar uma comunicação SSE. Essas conexões, por sua vez, serão gerenciadas pela classe Connection
:
const connection = new Connection()
app.get("/events", (req, res) => {
res
.writeHead(200, {
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
Connection: "keep-alive"
})
.write("\n")
const EVENTS_INTERVAL = 5000
const userId = req.query.user
const connectionId = connection.registerUser(userId, res)
setInterval(() => {
res.write(`data: ${JSON.stringify(connection.connectedUsers)}\n\n`)
}, EVENTS_INTERVAL)
req.on("close", () => connection.removeUser(userId, connectionId))
})
Aqui podemos destacar três pontos importantes:
-
O endpoint
/events
deve responder ao cliente com o cabeçalhoContent-Type: text/event-stream
. Isso permite que os clientes reconheçam a comunicação SSE e criem um objetoEventSource
para estabelecer a conexão. -
A cada cinco segundos, o servidor enviará uma resposta em forma de evento para os clientes conectados, informando quais usuários estabeleceram conexão.
-
Conexões encerradas podem ser monitoradas utilizando o evento
req.on("close", () => {})
, permitindo que usuários desconectados sejam removidos do mapa de conexões.
Nosso back-end está pronto! Agora podemos voltar nossa atenção para o front-end.
Como o servidor já disponibiliza um endpoint para conexões SSE, cabe ao cliente requisitar esse endereço e aguardar o recebimento dos eventos enviados.
No arquivo index.html
, vamos criar:
- Um campo de texto para o usuário informar seu UserID;
- Dois botões: um para iniciar e outro para encerrar a conexão SSE;
- Uma tag
<ul>
para exibir a lista de usuários conectados, que será atualizada com os eventos enviados pelo servidor.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Server Sent Events Demo</title>
</head>
<body>
<h1>Server Sent Events Demo (SSE)</h1>
<input type="text" id="userIdTxt" placeholder="User ID" />
<input type="button" id="connectBtn" value="Connect" />
<input type="button" id="closeBtn" value="Close" disabled />
<ul id="users"></ul>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
Por fim, no arquivo script.js
para isolar a lógica de manipulação dos componentes e a criação do objeto EventSource
.
let eventSource = null
const userIdInput = document.querySelector("#userIdTxt")
const connectButton = document.querySelector("#connectBtn")
const closeButton = document.querySelector("#closeBtn")
const ulUsers = document.querySelector("#users")
connectButton.addEventListener("click", startConnection)
closeButton.addEventListener("click", closeConnection)
function startConnection() {
const userId = userIdInput.value.trim()
if (!userId) {
alert("Please enter a User ID")
return
}
eventSource = new EventSource(`/events?user=${userId}`)
connectButton.setAttribute("disabled", "")
closeButton.removeAttribute("disabled")
userIdInput.setAttribute("disabled", "")
eventSource.onmessage = (event) => {
const connectedUsers = JSON.parse(event.data)
updateUsersList(connectedUsers)
}
}
function updateUsersList(users) {
ulUsers.innerHTML = ""
users.forEach((user) => {
const liUser = document.createElement("li")
liUser.textContent = user
ulUsers.appendChild(liUser)
})
}
function closeConnection() {
eventSource.close()
ulUsers.innerHTML = ""
connectButton.removeAttribute("disabled")
userIdInput.removeAttribute("disabled")
closeButton.setAttribute("disabled", "")
}
Aqui, destacamos a criação do objeto eventSource
:
eventSource = new EventSource(`/events?user=${userId}`)
.
No construtor, ele espera como argumento um servidor que responda com o conteúdo text/event-stream
. Por sorte, já configuramos exatamente isso! 😜
A função eventSource.onmessage(event => {})
permite receber em tempo real todos os eventos enviados pelo servidor de forma unidirecional.
Para evitar que a conexão permaneça aberta indefinidamente, podemos utilizar eventSource.close()
, que fecha o canal de comunicação com o servidor. Alternativamente, a conexão também será encerrada ao fechar o navegador.
A magia do tempo real
Ao abrir duas abas da nossa aplicação no navegador, podemos validar o funcionamento da conexão em tempo real, observando as atualizações de usuários conectados em ação.

Note que, enquanto a conexão SSE permanecer aberta, os eventos continuarão sendo recebidos a cada 5 segundos, atualizando automaticamente a lista de usuários conectados ao servidor.

Com isso, concluímos a implementação de um sistema simples utilizando SSE para conexões em tempo real. Essa técnica é ideal para casos em que o servidor precisa enviar dados contínuos e unidirecionais para o cliente, como feeds, chats ou dashboards. Se você quiser conferir o código completo deste projeto, acesse meu repositório no GitHub: Server Sent Events.